import std.stdio, std.stream, std.cstream, std.string;
import bignum;

enum Opcode { CON, ADD, SUB, MUL, DIV, GOD, JPO, GTO, POP, PRI, RED, HAL, TOD, TTO }

class Instruction {
  Opcode opcode;
  Bignum operand;

  this() { }

  this(string s) {
    string t=strip(s);

    switch(t) {
      case "add":
        opcode=Opcode.ADD;
        break;
      case "sub":
        opcode=Opcode.SUB;
        break;
      case "mul":
        opcode=Opcode.MUL;
        break;
      case "div":
        opcode=Opcode.DIV;
        break;
      case "god":
        opcode=Opcode.GOD;
        break;
      case "jpo":
        opcode=Opcode.JPO;
        break;
      case "gto":
        opcode=Opcode.GTO;
        break;
      case "pop":
        opcode=Opcode.POP;
        break;
      case "pri":
        opcode=Opcode.PRI;
        break;
      case "red":
        opcode=Opcode.RED;
        break;
      case "hal":
        opcode=Opcode.HAL;
        break;
      default:
        try {
          opcode=Opcode.CON;
          operand=tobig(t);
        } catch(BigConvError e) {
          throw new Error("unknown instruction: "~s);
        }
    }
  }
}

class Instruction_t : Instruction {
  this(string s) {
    string t=strip(s);
    if(t=="tod") {
      super();
      opcode=Opcode.TOD;
    } else if(t=="tto") {
      super();
      opcode=Opcode.TTO;
    } else {
      super(t);
    }
  }
}

class Vm {
  const default_stack_size=1000;
  const code_default = "epro.code";
  const stack_max = 20000000;

  class Tape {
    Bignum[Bignum] t;
    Bignum Zero;

    this() {
      Zero=tobig(0);
    }

    Bignum get(Bignum i) {
      Bignum* p = i in t;
      return p !is null ? *p : Zero;
    }

    void set(Bignum i, Bignum b) {
      t[i]=b;
    }
  }

  Instruction[] code;
  Bignum[] stack;
  size_t pc, sp, max;
  int[] step;
  Bignum ecode;
  string emsg;
  Tape tape;

  this(string fname, bool usetape) {
    File f;
    try {
      if(fname) {
        try {
          f=new File(fname);
        } catch(OpenException e) {
          f=new File(fname~".code");
        }
      } else {
        f=new File(code_default);
      }
    } catch(OpenException e) {
      throw new OpenException(fname ? format("cannot open '%s' or '%s.code'",fname,fname)
                                    : format("cannot open '%s'",code_default));
    }

    while(!f.eof()) {
      string s=cast(string)f.readLine();
      if(!f.seekable && s=="" && f.eof()) break;
      code ~= usetape ? new Instruction_t(s) : new Instruction(s);
    }
    f.close();

    stack.length=default_stack_size;
    if(usetape) tape=new Tape();
  }

  void run(bool silent) {

    void chkstack() {
      if(sp<stack.length) {
        return;
      } else {
        if(stack.length==stack_max) throw new Exception("");
        size_t j = stack.length*2+1;
        stack.length = j<=stack_max ? j : stack_max;
      }
    }

    writefln("*** START ***");

run:
    while(true) {
      if(pc>=code.length) {
        emsg="cannot fetch next instruction";
        break;
      }

      switch(code[pc].opcode) {
        case Opcode.CON:
          try {
            chkstack();
          } catch(Exception e) {
            emsg="stack out of bounds (con)";
            break run;
          }
          stack[sp]=code[pc].operand;
          sp++; pc++;
          if(max<sp) max=sp;
          break;
        case Opcode.ADD:
          if(sp<2) {
            emsg="stack is empty (add)";
            break run;
          }
          stack[sp-2]=bigadd(stack[sp-2],stack[sp-1]);
          sp--; pc++;
          break;
        case Opcode.SUB:
          if(sp<2) {
            emsg="stack is empty (sub)";
            break run;
          }
          stack[sp-2]=bigsub(stack[sp-2],stack[sp-1]);
          sp--; pc++;
          break;
        case Opcode.MUL:
          if(sp<2) {
            emsg="stack is empty (mul)";
            break run;
          }
          stack[sp-2]=bigmul(stack[sp-2],stack[sp-1]);
          sp--; pc++;
          break;
        case Opcode.DIV:
          if(sp<2) {
            emsg="stack is empty (div)";
            break run;
          }
          try {
            stack[sp-2]=bigdiv(stack[sp-2],stack[sp-1]);
          } catch(ZeroDivException e) {
            emsg="division by zero";
            break run;
          }
          sp--; pc++;
          break;
        case Opcode.GOD:
          if(sp==0) {
            emsg="stack is empty (god)";
            break run;
          }
          size_t s;
          try {
            s=toint!(size_t)(stack[sp-1]);
            if(sp-1<=s) throw new IntConvError();
          } catch(IntConvError e) {
            emsg="stack out of bounds (god)";
            break run;
          }
          stack[sp-1]=stack[s];
          pc++;
          break;
        case Opcode.JPO:
          if(sp<2) {
            emsg="stack is empty (jpo)";
            break run;
          }
          if(stack[sp-2].sign>0) {
            try {
              pc=toint!(size_t)(stack[sp-1]);
            } catch(IntConvError e) {
              pc=size_t.max; //code[size_t.max]はないはず
            }
          } else {
            pc++;
          }
          sp-=2;
          break;
        case Opcode.GTO:
          if(sp<2) {
            emsg="stack is empty (gto)";
            break run;
          }
          size_t s;
          try {
            s=toint!(size_t)(stack[sp-1]);
            if(sp-2<=s) throw new IntConvError();
          } catch(IntConvError e) {
            emsg="stack out of bounds (gto)";
            break run;
          }
          stack[s]=stack[sp-2];
          sp-=2;
          pc++;
          break;
        case Opcode.POP:
          if(sp==0) {
            emsg="stack is empty (pop)";
            break run;
          }
          sp--; pc++;
          break;
        case Opcode.TOD:
          if(sp==0) {
            emsg="stack is empty (tod)";
            break run;
          }
          stack[sp-1] = tape.get(stack[sp-1]);
          pc++;
          break;
        case Opcode.TTO:
          if(sp<2) {
            emsg="stack is empty (tto)";
            break run;
          }
          tape.set(stack[sp-1], stack[sp-2]);
          sp-=2;
          pc++;
          break;
        case Opcode.PRI:
          if(sp==0) {
            emsg="stack is empty (pri)";
            break run;
          }
          writefln(stack[sp-1]);
          sp--; pc++;
          break;
        case Opcode.RED:
          Bignum b;
          if(silent) {
            string t=cast(string)din.readLine();
            if(t=="" && din.eof()) {
              emsg="end of input";
              break run;
            }
            try {
              b=tobig(strip(t));
            } catch(BigConvError e) {
              emsg="input is not an integer!";
              break run;
            }
          } else {
            while(true) {
              writefln("input an integer?");
              string t=cast(string)din.readLine();
              if(t=="" && din.eof()) {
                emsg="end of input";
                break run;
              }
              try {
                b=tobig(strip(t));
                writefln("OK");
                break;
              } catch(BigConvError e) {
                continue;
              }
            }
          }
          try {
            chkstack();
          } catch(Exception e) {
            emsg="stack out of bounds (red)";
            break run;
          }
          stack[sp]=b;
          sp++; pc++;
          if(max<sp) max=sp;
          break;
        case Opcode.HAL:
          if(sp==0) {
            emsg="stack is empty (hal)";
            break run;
          }
          ecode=stack[sp-1];
          sp--;
          break run;
        default:
          throw new Error("never reach");
      }

      step=absinc(step);
    }

    if(emsg) {
      writefln("*** ERROR [%s] *** (%s steps, %d stacks)",emsg,tos(step),max);
    } else if(ecode.sign!=0) {
      writefln("*** HALT [%s] *** (%s steps, %d stacks)",ecode,tos(step),max);
    } else {
      writefln("*** HALT *** (%s steps, %d stacks)",tos(step),max);
    }
  }
}

void main(string[] args) {
  string fname;
  bool silent=false;
  bool usetape=false;

  for(int i=1; i<args.length; i++) {
    string s = args[i];
    if(!silent && s=="-s") {
      silent=true;
    } else if(!usetape && s=="-t") {
      usetape=true;
    } else {
      if(fname) {
        writefln("usage: depro [-s] [-t] [code]");
        return 1;
      }
      fname=s;
    }
  }

  try {
    Vm vm=new Vm(fname, usetape);
    vm.run(silent);
  } catch(Exception e) {
    writefln(e);
    return 1;
  }
}
